package com.joshua.spring.integration;

import com.joshua.spring.entity.em.AuthTypeEnum;
import com.joshua.spring.integration.authenticator.IntegrationAuthenticator;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * com.joshua.spring.business.filters -- IntegrationAuthenticationFilter
 * description: 定义集成认证拦截器
 *
 * @author <a href="mailto:joshualwork@163.com">joshua_liu</a>
 * @date 2020/1/6 10:36
 */
@Component
public class IntegrationAuthenticationFilter extends GenericFilterBean implements ApplicationContextAware {
    private static final String AUTH_TYPE_PARAM_NAME = "authType";
    /**
     * 注意，此处过滤的url必须是/oauth/token，而不能是/auth/token
     */
    private static final String OAUTH_TOKEN_URL = "/oauth/token";

    private RequestMatcher requestMatcher;
    private ApplicationContext applicationContext;
    private Collection<IntegrationAuthenticator> authenticators;

    public IntegrationAuthenticationFilter() {
        this.requestMatcher = new OrRequestMatcher(
                new AntPathRequestMatcher(OAUTH_TOKEN_URL, HttpMethod.POST.toString()),
                new AntPathRequestMatcher(OAUTH_TOKEN_URL, HttpMethod.GET.toString()));
    }

    /**
     * description: 对获取token的请求做拦截,组装{@link IntegrationAuthentication}
     * 把所有的请求参数放入{@link IntegrationAuthentication#setAuthParameters(Map)}中
     * 为了在{@link IntegrationAuthenticator#authenticate(IntegrationAuthentication)}
     * 的实现类中 认证时 获取需要的值。 eg:password/phone/code等。
     * 并把{@link IntegrationAuthentication} 放入{@link IntegrationAuthenticationContext}
     *
     * @author <a href="mailto:joshualwork@163.com">joshua_liu</a>
     * @date 2020/1/6 14:53
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (requestMatcher.matches(request)) {
            RequestParameterWrapper requestParameterWrapper = new RequestParameterWrapper(request);
            IntegrationAuthentication integrationAuthentication = new IntegrationAuthentication();
            String pwdValue = requestParameterWrapper.getParameter(AUTH_TYPE_PARAM_NAME);
            AuthTypeEnum authType = StringUtils.isEmpty(pwdValue) ?
                    AuthTypeEnum.PASSWORD : AuthTypeEnum.getInstance(pwdValue);
            integrationAuthentication.setAuthType(authType);
            integrationAuthentication.setAuthParameters(requestParameterWrapper.getParameterMap());
            IntegrationAuthenticationContext.set(integrationAuthentication);
            try {
                this.prepare(integrationAuthentication);
                filterChain.doFilter(requestParameterWrapper, servletResponse);
                this.complete(integrationAuthentication);
            } finally {
                IntegrationAuthenticationContext.clear();
            }
        } else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * description: 认证前回调,从ApplicationContext中
     * 获取当前{@link IntegrationAuthenticator}的实现类
     * 并对集成认证实体做预处理
     *
     * @param integrationAuthentication 集成认证实体
     * @author <a href="mailto:joshualwork@163.com">joshua_liu</a>
     * @date 2020/1/6 11:37
     */
    private void prepare(IntegrationAuthentication integrationAuthentication) {
        if (integrationAuthentication != null) {
            synchronized (this) {
                Map<String, IntegrationAuthenticator> map = applicationContext.getBeansOfType(IntegrationAuthenticator.class);
                if (map != null) {
                    this.authenticators = map.values();
                }
            }
        }
        if (this.authenticators == null) {
            this.authenticators = new ArrayList<>();
        }
        for (IntegrationAuthenticator authenticator : this.authenticators) {
            if (authenticator.support(integrationAuthentication)) {
                authenticator.prepare(integrationAuthentication);
            }
        }
    }

    /**
     * description: 认证结束后回调
     * 可做日志记录
     *
     * @param integrationAuthentication 集成认证实体
     * @author <a href="mailto:joshualwork@163.com">joshua_liu</a>
     * @date 2020/1/6 15:20
     */
    private void complete(IntegrationAuthentication integrationAuthentication) {
        for (IntegrationAuthenticator authenticator : authenticators) {
            if (authenticator.support(integrationAuthentication)) {
                authenticator.complete(integrationAuthentication);
            }
        }
    }

    /**
     * 用途：在拦截时给Request添加参数
     * Cloud OAuth2 密码模式需要判断Request是否存在password参数，
     * 如果不存在会抛异常结束认证
     * 所以在调用doFilter方法前添加password参数
     */
    class RequestParameterWrapper extends HttpServletRequestWrapper {
        private Map<String, String[]> params = new HashMap<>();

        public RequestParameterWrapper(HttpServletRequest request) {
            super(request);
            this.params.putAll(request.getParameterMap());
        }

        public RequestParameterWrapper(HttpServletRequest request, Map<String, Object> extraParams) {
            this(request);
            addParameters(extraParams);
        }

        public void addParameters(Map<String, Object> extraParams) {
            for (Map.Entry<String, Object> entry : extraParams.entrySet()) {
                addParameter(entry.getKey(), entry.getValue());
            }
        }

        @Override
        public String getParameter(String name) {
            String[] values = params.get(name);
            if (values == null || values.length == 0) {
                return null;
            }
            return values[0];
        }

        @Override
        public String[] getParameterValues(String name) {
            return params.get(name);
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            return params;
        }

        public void addParameter(String name, Object value) {
            if (value != null) {
                if (value instanceof String[]) {
                    params.put(name, (String[]) value);
                } else if (value instanceof String) {
                    params.put(name, new String[]{(String) value});
                } else {
                    params.put(name, new String[]{String.valueOf(value)});
                }
            }
        }

    }

}
